<?php

/**
 * AfroSoft ACL API
 *
 * @package API
 *
 * @author AfroSoft
 * @version @build.version@ (Build @build.number@)
 * @license http://www.mozilla.org/MPL/MPL-1.1.txt MPL 1.1
 * @copyright Copyright (c) AfroSoft
 */

/* * **** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the AfroSoft ACL API.
 *
 * The Initial Developer of the Original Code is AfroSoft.
 * Portions created by the Initial Developer are Copyright (C) 2011 the Initial
 * Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Xavier L. <xavier.l@afrosoft.tk>
 *
 * ***** END LICENSE BLOCK ***** */

require_once 'Exception.php';

/**
 * @mainpage
 * Usage:
 *
 * Create a database object. Database objects do not need to be the ones
 * provided by the script, but must implement the ACL_DB_Interface interface.
 * This ensures that all the database functions called by the script are
 * implemented, or at least defined in the database object. This means that you
 * can create your own database object, or include the required functions in
 * your existing database object.
 * To create a database object, call the load_ACL_DB() function, and assign its
 * return value to a variable.
 * @code
 * $dbi = load_ACL_DB($parameters);
 * @endcode
 *
 * The dbi object must be set to the dbi variable inside the API class. This 
 * allegiates subsequent API calls.
 */

/**
 * This function loads and instanciate a database object for use with the API.
 *
 * The parameter used with this function is:
 * - interface: the name of the database layer to use. It must correspond
 * exactly to the file name and the class name. Defaults to 'mysql'
 *
 * Any parameter that must be passed to the database layer needs to be passed
 * to this function, as this function instanciates the database layer it loads.
 *
 * @param array $params
 */
function load_ACL_DB($params) {
    $params = array_merge(array(
        'interface' => 'mysql',
            ), $params);

    require_once 'DB/ACL_DB_' . $params['interface'] . '.php';

    $DBi = 'ACL_DB_' . $params['interface'];

    return new $DBi($params);
}

/**
 * This class contains the constants defining the various permission levels.
 */
class ACLPermission {
    /**
     * The permission level when a permission is unknown.
     */
    const UNKNOWN = -1;

    /**
     * The constant representing a deny.
     */
    const DENY = 0;

    /**
     * The constant representing an allow.
     */
    const ALLOW = 1;

    /**
     * The permission level for a user.
     */
    const T_USER = 2;

    /**
     * The permission level for a group.
     */
    const T_GROUP = 4;

    /**
     * The permission level for a default.
     */
    const T_DEFAULT = 8;
}

class ACLRequest {

    /**
     * The variable holding the database layer object.
     */
    static public $dbi;

    /**
     * This is the main function, it is used to check a single permission for a
     * given user and/or group. It can also be limited as to not load the default permission.
     *
     * @param array $param
     * @return const ACLPermission
     */
    static public function checkPermission(array $param) {
        if (empty(self::$dbi)) {
            throw new ACLDatabaseNotConnectedException;
        }

        $param = array_merge(array(
            'key' => null,
            'user' => null,
            'group' => null,
            'explicit' => false,
                ), $param);

        if ((empty($param['user']) && empty($param['group'])) || empty($param['key'])) {
            throw new MissingRequiredArgumentException(array('user OR group, key'));
        }

        $permData = array();

        try {
            $permData = self::$dbi->getPermission($param);
        } catch (ACLException $ACLe) {
            throw $ACLe;
        } catch (Exception $e) {
            throw $e;
        }

        if (!isset($permData['permission']) || empty($permData['type'])) {
            if ($param['explicit'] != true) {
                throw new PermissionNotSetException('An explicit permission for this user/group on the \'' . $param['key'] . '\' item was not set.');
            } else {
                return ACLPermission::UNKNOWN;
            }
        }

        $permission = null;
        switch ($permData['type']) {
            case 'user':
                $permission = ACLPermission::T_USER;
                break;
            case 'group':
                $permission = ACLPermission::T_GROUP;
                break;
            case 'default':
                $permission = ACLPermission::T_DEFAULT;
                break;
            default:
                throw new ACLException('Unknown permission source.');
        }
        switch ($permData['permission']) {
            case true:
                $permission |= ACLPermission::ALLOW;
                break;
            case false:
                $permission |= ACLPermission::DENY;
                break;
            default:
                throw new ACLException('Unknown permission result.');
        }

        return $permission;
    }

    /**
     * This is a function used to load in a single call, all explicit
     * permissions for a given user and/or group. It is useful when caching
     * permissions, i.e. saving them as a session variable. The returned object 
     * contains a list of all the specific permissions, whether they be allow 
     * or deny (no default permission stored).
     *
     * @param array $param
     * @return ACLSnapshot
     */
    static public function getAllPermissions(array $param) {
        if (empty(self::$dbi)) {
            throw new ACLDatabaseNotConnectedException;
        }

        $param = array_merge(array(
            'user' => null,
            'group' => null,
                ), $param);

        if (empty($param['user']) && empty($param['group'])) {
            throw new MissingListRequiredArgumentException(array('user', 'group'));
        }

        $snap = new ACLSnapshot($param);
        $temporaryPermission = ACLPermission::UNKNOWN;

        if (!empty($param['user'])) {
            $userPermData = array();
            try {
                $userPermData = self::$dbi->getAllPermissions(array('type' => 'user', 'id' => $param['user']));
            } catch (ACLException $ACLe) {
                throw $ACLe;
            } catch (Exception $e) {
                throw $e;
            }
            foreach ($userPermData as $id => $row) {
                $temporaryPermission = ACLPermission::T_USER;
                switch ($row['permission']) {
                    case 1:
                        $temporaryPermission |= ACLPermission::ALLOW;
                        break;
                    case 0:
                        $temporaryPermission |= ACLPermission::DENY;
                        break;
                    default:
                        throw new ACLException('The permission level \'' . $row['permission'] . '\' is invalid.');
                }
                $snap->addPermission(array('key' => $row['key'], 'permission' => $temporaryPermission));
            }
        }
        if (!empty($param['group'])) {
            $groupPermData = array();
            try {
                $groupPermData = self::$dbi->getAllPermissions(array('type' => 'group', 'id' => $param['group']));
            } catch (ACLException $ACLe) {
                throw $ACLe;
            } catch (Exception $e) {
                throw $e;
            }
            foreach ($groupPermData as $id => $row) {
                $temporaryPermission = ACLPermission::T_GROUP;
                switch ($row['permission']) {
                    case 1:
                        $temporaryPermission |= ACLPermission::ALLOW;
                        break;
                    case 0:
                        $temporaryPermission |= ACLPermission::DENY;
                        break;
                    default:
                        throw new ACLException('The permission level \'' . $row['permission'] . '\' is invalid.');
                }
                $snap->addPermission(array('key' => $row['key'], 'permission' => $temporaryPermission));
            }
        }

        return $snap;
    }

}

/**
 * This class is in a way a snapshot of a specific's user (and group) explicit
 * permission. This is why it is possible to add new permissions, but not to
 * modify existing ones. To modify existing permissions, a new ACLSnapshot
 * object must be created. It doesn't connect with the database if a
 * permission's state is unknown, by design. In that case, it is recommended
 * for the application to revert to the default ACLRequest::checkPermission()
 * method.
 */
class ACLSnapshot {

    private $permissions = array();
    private $user;
    private $group;

    /**
     * The constructor sets the initial local database and sets the various
     * options for the current permission snapshot.
     *
     * @param array $param
     */
    public function __construct(array $param) {
        $param = array_merge(array(
            'user' => null,
            'group' => null,
            'permissions' => null,
                ), $param);

        if (empty($param['user']) && empty($param['group'])) {
            throw new MissingListRequiredArgumentException(array('user', 'group'));
        }

        $this->user = $param['user'];
        $this->group = $param['group'];

        if (!empty($param['permissions']) && is_array($param['permissions'])) {
            foreach ($param['permissions'] as $key => $permission) {
                $this->addPermission(array('key' => $key, 'permission' => $permission));
            }
        }
    }

    /**
     * The addPermission function gives the ability to add permissions to
     * the local database. It is not intended for end-user use.
     *
     * @internal
     * @param array $param
     */
    public function addPermission(array $param) {
        $param = array_merge(array(
            'key' => null,
            'permission' => null,
                ), $param);

        if (array_key_exists($param['key'], $this->permissions)) {
            throw new PermissionAlreadySetException('Permission already stored. Can\'t be edited.');
        }

        $this->permissions[$param['key']] = $param['permission'];
    }

    /**
     * The checkPermission function checks if the specified item has an
     * explicit permission in the local storage (does not connect to the
     * database). If it does, it returns said permission. If not, behaviour
     * is controlled by the value of explicit. If explicit is true, an
     * exception is thrown, if not, the {@link ACLPermission::UNKNOWN} constant
     * is returned.
     *
     * @param array $param
     * @return const
     */
    public function checkPermission(array $param) {
        $param = array_merge(array(
            'key' => null,
            'explicit' => false,
                ), $param);

        if (empty($param['key'])) {
            throw new MissingRequiredArgumentException(array('key'));
        }
        if (array_key_exists($param['key'], $this->permissions)) {
            return $this->permissions[$param['key']];
        }

        if ($param['explicit'] == true) {
            throw new ItemNotDefinedException('No permission under the specified key have been specifically set for this user/group.');
        }
        return ACLPermission::UNKNOWN;
    }

}
